• Jump To … +
    LICENSE.md README.md index.md ast.js identifiers.js parser.js stats.js trace.js utilities.js
  • stats.js

  • §

    Stats Class Overview

  • §

    The Stats class provides basic profiling capabilities for the parser’s performance for a given SABNF grammar and input string. It tracks how frequently each parse tree node type is invoked during parsing, offering insight into parser behavior and grammar optimization.

  • §

    Key Features

  • §
    • Node Type Profiling Counts the number of times each parse tree node type is triggered during parsing.
  • §
    • Rule-Level Granularity Provides individual hit counts for each named rule and user-defined terminal (UDT) node.
  • §
    • Grammar Diagnostics Useful for identifying hotspots, optimizing grammar structure, and understanding parser performance.
  • §

    Use Case

  • §

    Ideal for developers seeking lightweight diagnostics and performance insights during grammar development or parser tuning.

    import { identifiers as id } from '../src/identifiers.js';
    export { Stats };
    class Stats {
      #FILENAME = 'stats.js: ';
      #rules;
      #udts;
      #totals;
      #stats = [];
      #ruleStats = [];
      #udtStats = [];
  • §

    Called by the user after the parser has completed. Displays hit counts and totals for each and all node types.

      displayStats = () => {
        let out = '';
        const displayRow = (op, m, e, n, t) => {
          this.#totals.match += m;
          this.#totals.empty += e;
          this.#totals.nomatch += n;
          this.#totals.total += t;
          const mm = this.#normalize(m);
          const ee = this.#normalize(e);
          const nn = this.#normalize(n);
          const tt = this.#normalize(t);
          return `${op} | ${mm} | ${ee} | ${nn} | ${tt} |\n`;
        };
        out += '          OPERATOR STATS\n';
        out += '      |   MATCH |   EMPTY | NOMATCH |   TOTAL |\n';
        out += displayRow(
          '  ALT',
          this.#stats[id.ALT].match,
          this.#stats[id.ALT].empty,
          this.#stats[id.ALT].nomatch,
          this.#stats[id.ALT].total
        );
        out += displayRow(
          '  CAT',
          this.#stats[id.CAT].match,
          this.#stats[id.CAT].empty,
          this.#stats[id.CAT].nomatch,
          this.#stats[id.CAT].total
        );
        out += displayRow(
          '  REP',
          this.#stats[id.REP].match,
          this.#stats[id.REP].empty,
          this.#stats[id.REP].nomatch,
          this.#stats[id.REP].total
        );
        out += displayRow(
          '  RNM',
          this.#stats[id.RNM].match,
          this.#stats[id.RNM].empty,
          this.#stats[id.RNM].nomatch,
          this.#stats[id.RNM].total
        );
        out += displayRow(
          '  TRG',
          this.#stats[id.TRG].match,
          this.#stats[id.TRG].empty,
          this.#stats[id.TRG].nomatch,
          this.#stats[id.TRG].total
        );
        out += displayRow(
          '  TBS',
          this.#stats[id.TBS].match,
          this.#stats[id.TBS].empty,
          this.#stats[id.TBS].nomatch,
          this.#stats[id.TBS].total
        );
        out += displayRow(
          '  TLS',
          this.#stats[id.TLS].match,
          this.#stats[id.TLS].empty,
          this.#stats[id.TLS].nomatch,
          this.#stats[id.TLS].total
        );
        out += displayRow(
          '  UDT',
          this.#stats[id.UDT].match,
          this.#stats[id.UDT].empty,
          this.#stats[id.UDT].nomatch,
          this.#stats[id.UDT].total
        );
        out += displayRow(
          '  AND',
          this.#stats[id.AND].match,
          this.#stats[id.AND].empty,
          this.#stats[id.AND].nomatch,
          this.#stats[id.AND].total
        );
        out += displayRow(
          '  NOT',
          this.#stats[id.NOT].match,
          this.#stats[id.NOT].empty,
          this.#stats[id.NOT].nomatch,
          this.#stats[id.NOT].total
        );
        out += displayRow('TOTAL', this.#totals.match, this.#totals.empty, this.#totals.nomatch, this.#totals.total);
        return out;
      };
  • §

    Called by the user after parser completion.

    • @param {string | undefined} type
      • ‘hits’(default) - rule/UDT names ordered by hit count descending
      • ‘alpha’ - rule/UDT names ordered alphabetically
      • ‘index’ - rule/UDT names ordered by index (the order they appear in the SABNF grammar)
      displayHits = (type) => {
        let out = '';
        const displayRow = (m, e, n, t, name) => {
          this.#totals.match += m;
          this.#totals.empty += e;
          this.#totals.nomatch += n;
          this.#totals.total += t;
          const mm = this.#normalize(m);
          const ee = this.#normalize(e);
          const nn = this.#normalize(n);
          const tt = this.#normalize(t);
          return `| ${mm} | ${ee} | ${nn} | ${tt} | ${name}\n`;
        };
        if (typeof type === 'string' && type.toLowerCase()[0] === 'a') {
          this.#ruleStats.sort(this.#sortAlpha);
          this.#udtStats.sort(this.#sortAlpha);
          out += '    RULES/UDTS ALPHABETICALLY\n';
        } else if (typeof type === 'string' && type.toLowerCase()[0] === 'i') {
          this.#ruleStats.sort(this.#sortIndex);
          this.#udtStats.sort(this.#sortIndex);
          out += '    RULES/UDTS BY INDEX\n';
        } else {
          this.#ruleStats.sort(this.#sortHits);
          this.#udtStats.sort(this.#sortHits);
          out += '    RULES/UDTS BY HIT COUNT\n';
        }
        out += '|   MATCH |   EMPTY | NOMATCH |   TOTAL | NAME\n';
        for (let i = 0; i < this.#ruleStats.length; i += 1) {
          let r = this.#ruleStats[i];
          if (r.total) {
            out += displayRow(r.match, r.empty, r.nomatch, r.total, r.name);
          }
        }
        for (let i = 0; i < this.#udtStats.length; i += 1) {
          let r = this.#udtStats[i];
          if (r.total) {
            out += displayRow(r.match, r.empty, r.nomatch, r.total, r.name);
          }
        }
        return out;
      };
  • §

    Called by parser to initialize the Stats object.

      init = (r, u) => {
        this.#rules = r;
        this.#udts = u;
        this.#clear();
      };
  • §

    Called by the parser after each node has been traversed.

      collect = (op, sys) => {
        this.#incStat(this.#totals, sys.state, sys.phraseLength);
        this.#incStat(this.#stats[op.type], sys.state, sys.phraseLength);
        if (op.type === id.RNM) {
          this.#incStat(this.#ruleStats[op.index], sys.state, sys.phraseLength);
        }
        if (op.type === id.UDT) {
          this.#incStat(this.#udtStats[op.index], sys.state, sys.phraseLength);
        }
      };
  • §

    Zero out all stats.

      #clear = () => {
        class EmptyStat {
          constructor() {
            this.empty = 0;
            this.match = 0;
            this.nomatch = 0;
            this.total = 0;
          }
        }
        this.#stats.length = 0;
        this.#totals = new EmptyStat();
        this.#stats[id.ALT] = new EmptyStat();
        this.#stats[id.CAT] = new EmptyStat();
        this.#stats[id.REP] = new EmptyStat();
        this.#stats[id.RNM] = new EmptyStat();
        this.#stats[id.TRG] = new EmptyStat();
        this.#stats[id.TBS] = new EmptyStat();
        this.#stats[id.TLS] = new EmptyStat();
        this.#stats[id.UDT] = new EmptyStat();
        this.#stats[id.AND] = new EmptyStat();
        this.#stats[id.NOT] = new EmptyStat();
        this.#ruleStats.length = 0;
        for (let i = 0; i < this.#rules.length; i += 1) {
          this.#ruleStats.push({
            empty: 0,
            match: 0,
            nomatch: 0,
            total: 0,
            name: this.#rules[i].name,
            lower: this.#rules[i].lower,
            index: this.#rules[i].index,
          });
        }
        if (this.#udts.length > 0) {
          this.#udtStats.length = 0;
          for (let i = 0; i < this.#udts.length; i += 1) {
            this.#udtStats.push({
              empty: 0,
              match: 0,
              nomatch: 0,
              total: 0,
              name: this.#udts[i].name,
              lower: this.#udts[i].lower,
              index: this.#udts[i].index,
            });
          }
        }
      };
  • §

    Set leading spaces to keep the count display at a fixed number of characters.

      #normalize = (n) => {
        if (n < 10) {
          return `      ${n}`;
        }
        if (n < 100) {
          return `     ${n}`;
        }
        if (n < 1000) {
          return `    ${n}`;
        }
        if (n < 10000) {
          return `   ${n}`;
        }
        if (n < 100000) {
          return `  ${n}`;
        }
        if (n < 1000000) {
          return ` ${n}`;
        }
        return `${n}`;
      };
  • §

    The sort callback for alphabetical sorting.

      #sortAlpha = (lhs, rhs) => {
        if (lhs.lower < rhs.lower) {
          return -1;
        }
        if (lhs.lower > rhs.lower) {
          return 1;
        }
        return 0;
      };
  • §

    The sort callback for hit count sorting.

      #sortHits = (lhs, rhs) => {
        if (lhs.total < rhs.total) {
          return 1;
        }
        if (lhs.total > rhs.total) {
          return -1;
        }
        return this.#sortAlpha(lhs, rhs);
      };
  • §

    The sort callback for index sorting.

      #sortIndex = (lhs, rhs) => {
        if (lhs.index < rhs.index) {
          return -1;
        }
        if (lhs.index > rhs.index) {
          return 1;
        }
        return 0;
      };
  • §

    Increment the designated operator hit count by one.

      #incStat = (stat, state) => {
        stat.total += 1;
        switch (state) {
          case id.EMPTY:
            stat.empty += 1;
            break;
          case id.MATCH:
            stat.match += 1;
            break;
          case id.NOMATCH:
            stat.nomatch += 1;
            break;
          default:
            throw new Error(`${this.#FILENAME}collect(): this.#incStat(): unrecognized state: ${state}`);
        }
      };
    }